using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;

namespace ModelContextProtocol.Tests.Configuration;

public partial class UrlElicitationTests(ITestOutputHelper testOutputHelper) : ClientServerTestBase(testOutputHelper)
{
    protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
    {
        mcpServerBuilder.WithCallToolHandler(async (request, cancellationToken) =>
        {
            Assert.NotNull(request.Params);

            if (request.Params.Name == "TestUrlElicitation")
            {
                var result = await request.Server.ElicitAsync(new()
                    {
                        Mode = "url",
                        ElicitationId = "test-elicitation-id",
                        Url = $"https://auth.example.com/oauth/authorize?state=test-elicitation-id",
                        Message = "Please authorize access to your account by logging in through your browser.",
                    },
                    cancellationToken);

                // For URL mode, we expect the client to accept (consent to navigate)
                // The actual OAuth flow happens out-of-band
                Assert.Equal("accept", result.Action);

                await request.Server.SendNotificationAsync(
                    NotificationMethods.ElicitationCompleteNotification,
                    new ElicitationCompleteNotificationParams
                    {
                        ElicitationId = "test-elicitation-id",
                    },
                    McpJsonUtilities.DefaultOptions,
                    cancellationToken);

                return new CallToolResult
                {
                    Content = [new TextContentBlock { Text = "authorization-pending:test-elicitation-id" }],
                };
            }
            else if (request.Params.Name == "TestUrlElicitationDecline")
            {
                var elicitationId = Guid.NewGuid().ToString();
                var result = await request.Server.ElicitAsync(new()
                    {
                        Mode = "url",
                        ElicitationId = "elicitation-declined-id",
                        Url = $"https://payment.example.com/pay?transaction=elicitation-declined-id",
                        Message = "Please complete payment in your browser.",
                    },
                    cancellationToken);

                // User declined
                Assert.Equal("decline", result.Action);

                return new CallToolResult
                {
                    Content = [new TextContentBlock { Text = "payment-declined" }],
                };
            }
            else if (request.Params.Name == "TestUrlElicitationCancel")
            {
                var elicitationId = Guid.NewGuid().ToString();
                var result = await request.Server.ElicitAsync(new()
                    {
                        Mode = "url",
                        ElicitationId = "elicitation-canceled-id",
                        Url = $"https://verify.example.com/verify?id=elicitation-canceled-id",
                        Message = "Please verify your identity.",
                    },
                    cancellationToken);

                // User canceled (dismissed without explicit choice)
                Assert.Equal("cancel", result.Action);

                return new CallToolResult
                {
                    Content = [new TextContentBlock { Text = "verification-canceled" }],
                };
            }
            else if (request.Params.Name == "TestUrlElicitationRequired")
            {
                throw new UrlElicitationRequiredException(
                    "Authorization is required to continue.",
                    new[]
                    {
                        new ElicitRequestParams
                        {
                            Mode = "url",
                            ElicitationId = "elicitation-required-id",
                            Url = "https://auth.example.com/connect?elicitationId=elicitation-required-id",
                            Message = "Authorization is required to access Example Co.",
                        }
                    });
            }
            else if (request.Params.Name == "TestUrlElicitationMissingId")
            {
                try
                {
                    await request.Server.ElicitAsync(new()
                        {
                            Mode = "url",
                            Url = "https://missing-id.example.com/oauth",
                            Message = "URL elicitation without ID should fail.",
                        },
                        cancellationToken);
                }
                catch (ArgumentException ex)
                {
                    throw new McpException(ex.Message);
                }

                return new CallToolResult
                {
                    Content = [new TextContentBlock { Text = "missing-id-succeeded" }],
                };
            }
            else if (request.Params.Name == "ProbeUrlCapability")
            {
                try
                {
                    await request.Server.ElicitAsync(new()
                        {
                            Mode = "url",
                            ElicitationId = Guid.NewGuid().ToString(),
                            Url = "https://probe.example.com/oauth",
                            Message = "Capability probe for url mode.",
                        },
                        cancellationToken);

                    return new CallToolResult
                    {
                        Content = [new TextContentBlock { Text = "url-allowed" }],
                    };
                }
                catch (InvalidOperationException ex)
                {
                    throw new McpException(ex.Message);
                }
            }
            else if (request.Params.Name == "ProbeFormCapability")
            {
                try
                {
                    var elicitationResult = await request.Server.ElicitAsync(new()
                        {
                            Message = "Capability probe for form mode.",
                            RequestedSchema = new(),
                        },
                        cancellationToken: cancellationToken);

                    return new CallToolResult
                    {
                        Content = [new TextContentBlock { Text = $"form-allowed:{elicitationResult.Action}" }],
                    };
                }
                catch (InvalidOperationException ex)
                {
                    throw new McpException(ex.Message);
                }
            }

            Assert.Fail($"Unexpected tool name: {request.Params.Name}");
            return new CallToolResult { Content = [] };
        });
    }

    [Fact]
    public async Task Can_Elicit_OutOfBand_With_Url()
    {
        string? capturedElicitationId = null;
        string? capturedUrl = null;
        string? capturedMessage = null;
        var completionNotification = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously);

        await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
        {
            Capabilities = new ClientCapabilities
            {
                // Explicitly declare support for both modes
                Elicitation = new ElicitationCapability
                {
                    Form = new FormElicitationCapability(),
                    Url = new UrlElicitationCapability()
                }
            },
            Handlers = new McpClientHandlers()
            {
                ElicitationHandler = async (request, cancellationToken) =>
                {
                    Assert.NotNull(request);

                    // Verify this is an URL mode elicitation request
                    Assert.NotNull(request.Mode);
                    Assert.Equal("url", request.Mode);

                    // Capture the request details
                    capturedElicitationId = request.ElicitationId;
                    capturedUrl = request.Url;
                    capturedMessage = request.Message;

                    // Verify URL-specific fields
                    Assert.NotNull(request.ElicitationId);
                    Assert.NotNull(request.Url);
                    Assert.StartsWith("https://auth.example.com/oauth/authorize", request.Url);
                    Assert.Contains("state=", request.Url);
                    Assert.Equal("Please authorize access to your account by logging in through your browser.", request.Message);

                    // Verify that RequestedSchema is null for URL mode
                    Assert.Null(request.RequestedSchema);

                    // Simulate user consent to navigate to the URL
                    // In a real implementation, the client would:
                    // 1. Display a consent dialog showing the URL and message
                    // 2. Open the URL in the system browser
                    // 3. Return "accept" to indicate user consented

                    // Return accept (user consented to navigate)
                    return new ElicitResult
                    {
                        Action = "accept",
                        // Note: No Content for URL mode - the actual interaction happens out-of-band
                    };
                }
            }
        });

        await using var completionHandler = client.RegisterNotificationHandler(
            NotificationMethods.ElicitationCompleteNotification,
            async (notification, cancellationToken) =>
            {
                var payload = notification.Params?.Deserialize<ElicitationCompleteNotificationParams>(McpJsonUtilities.DefaultOptions);
                if (payload is not null)
                {
                    completionNotification.TrySetResult(payload.ElicitationId);
                }

                await Task.CompletedTask;
            });

        var result = await client.CallToolAsync("TestUrlElicitation", cancellationToken: TestContext.Current.CancellationToken);

        // Verify the tool completed successfully
        Assert.Single(result.Content);
        var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
        Assert.StartsWith("authorization-pending:", textContent.Text);

        // Verify we captured all the expected URL mode elicitation data
        Assert.NotNull(capturedElicitationId);
        Assert.NotNull(capturedUrl);
        Assert.NotNull(capturedMessage);
        Assert.Contains(capturedElicitationId, capturedUrl);

        var notifiedElicitationId = await completionNotification.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
        Assert.Equal(capturedElicitationId, notifiedElicitationId);
    }

    [Fact]
    public async Task UrlElicitation_User_Can_Decline()
    {
        await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
        {
            Capabilities = new ClientCapabilities
            {
                Elicitation = new ElicitationCapability
                {
                    Url = new UrlElicitationCapability()
                }
            },
            Handlers = new McpClientHandlers()
            {
                ElicitationHandler = async (request, cancellationToken) =>
                {
                    Assert.NotNull(request);
                    Assert.NotNull(request.Mode);
                    Assert.Equal("url", request.Mode);
                    Assert.Contains("payment.example.com", request.Url);

                    // Simulate user declining to navigate to the URL
                    // This might happen if user doesn't trust the URL or doesn't want to proceed
                    return new ElicitResult
                    {
                        Action = "decline"
                    };
                }
            }
        });

        var result = await client.CallToolAsync("TestUrlElicitationDecline", cancellationToken: TestContext.Current.CancellationToken);

        // Server should handle decline gracefully
        Assert.Single(result.Content);
        var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
        Assert.Equal("payment-declined", textContent.Text);
    }

    [Fact]
    public async Task UrlElicitation_User_Can_Cancel()
    {
        await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
        {
            Capabilities = new ClientCapabilities
            {
                Elicitation = new ElicitationCapability
                {
                    Url = new UrlElicitationCapability()
                }
            },
            Handlers = new McpClientHandlers()
            {
                ElicitationHandler = async (request, cancellationToken) =>
                {
                    Assert.NotNull(request);
                    Assert.NotNull(request.Mode);
                    Assert.Equal("url", request.Mode);
                    Assert.Contains("verify.example.com", request.Url);

                    // Simulate user canceling (dismissing dialog without explicit choice)
                    return new ElicitResult
                    {
                        Action = "cancel"
                    };
                }
            }
        });

        var result = await client.CallToolAsync("TestUrlElicitationCancel", cancellationToken: TestContext.Current.CancellationToken);

        // Server should handle cancellation
        Assert.Single(result.Content);
        var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
        Assert.Equal("verification-canceled", textContent.Text);
    }

    [Fact]
    public async Task UrlElicitation_Defaults_To_Unsupported_When_Handler_Provided()
    {
        await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
        {
            Handlers = new McpClientHandlers()
            {
                ElicitationHandler = (request, cancellationToken) =>
                {
                    throw new InvalidOperationException("URL handler should not be invoked in default mode.");
                },
            }
        });

        var defaultCapability = AssertServerElicitationCapability();
        Assert.NotNull(defaultCapability.Form);
        Assert.Null(defaultCapability.Url);

        var result = await client.CallToolAsync("ProbeUrlCapability", cancellationToken: TestContext.Current.CancellationToken);

        Assert.True(result.IsError);
        var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
        Assert.Equal("An error occurred invoking 'ProbeUrlCapability': Client does not support URL mode elicitation requests.", textContent.Text);
    }

    [Fact]
    public async Task FormElicitation_Defaults_To_Supported_When_Handler_Provided()
    {
        await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
        {
            Handlers = new McpClientHandlers()
            {
                ElicitationHandler = (_, _) => new ValueTask<ElicitResult>(new ElicitResult { Action = "decline" }),
            }
        });

        var capability = AssertServerElicitationCapability();
        Assert.NotNull(capability.Form);
        Assert.Null(capability.Url);

        var result = await client.CallToolAsync("ProbeFormCapability", cancellationToken: TestContext.Current.CancellationToken);

        var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
        Assert.Equal("form-allowed:decline", textContent.Text);
    }

    [Fact]
    public async Task UrlElicitation_BlankCapability_Allows_Only_Form()
    {
        await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
        {
            Capabilities = new ClientCapabilities
            {
                Elicitation = new ElicitationCapability(),
            },
            Handlers = new McpClientHandlers()
            {
                ElicitationHandler = (_, _) => new ValueTask<ElicitResult>(new ElicitResult { Action = "decline" }),
            }
        });

        var capability = AssertServerElicitationCapability();
        Assert.NotNull(capability.Form);
        Assert.Null(capability.Url);

        var urlResult = await client.CallToolAsync("ProbeUrlCapability", cancellationToken: TestContext.Current.CancellationToken);
        Assert.True(urlResult.IsError);
        var urlTextContent = Assert.IsType<TextContentBlock>(urlResult.Content[0]);
        Assert.Equal("An error occurred invoking 'ProbeUrlCapability': Client does not support URL mode elicitation requests.", urlTextContent.Text);

        var formResult = await client.CallToolAsync("ProbeFormCapability", cancellationToken: TestContext.Current.CancellationToken);
        var textContent = Assert.IsType<TextContentBlock>(formResult.Content[0]);
        Assert.Equal("form-allowed:decline", textContent.Text);
    }

    [Fact]
    public async Task FormElicitation_UrlOnlyCapability_NotSupported()
    {
        await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
        {
            Capabilities = new ClientCapabilities
            {
                Elicitation = new ElicitationCapability
                {
                    Url = new UrlElicitationCapability(),
                }
            },
            Handlers = new McpClientHandlers()
            {
                ElicitationHandler = (request, cancellationToken) =>
                {
                    Assert.NotNull(request);
                    Assert.Equal("url", request.Mode);
                    return new ValueTask<ElicitResult>(new ElicitResult { Action = "decline" });
                },
            }
        });

        var capability = AssertServerElicitationCapability();
        Assert.Null(capability.Form);
        Assert.NotNull(capability.Url);

        var urlResult = await client.CallToolAsync("ProbeUrlCapability", cancellationToken: TestContext.Current.CancellationToken);
        var urlText = Assert.IsType<TextContentBlock>(urlResult.Content[0]);
        Assert.Equal("url-allowed", urlText.Text);

        var formResult = await client.CallToolAsync("ProbeFormCapability", cancellationToken: TestContext.Current.CancellationToken);
        Assert.True(formResult.IsError);
        var formText = Assert.IsType<TextContentBlock>(formResult.Content[0]);
        Assert.Equal("An error occurred invoking 'ProbeFormCapability': Client does not support form mode elicitation requests.", formText.Text);
    }

    [Fact]
    public async Task UrlElicitation_Requires_ElicitationId_For_Url_Mode()
    {
        var elicitationHandlerCalled = false;

        await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
        {
            Capabilities = new ClientCapabilities
            {
                Elicitation = new ElicitationCapability
                {
                    Url = new UrlElicitationCapability(),
                }
            },
            Handlers = new McpClientHandlers()
            {
                ElicitationHandler = (request, cancellationToken) =>
                {
                    elicitationHandlerCalled = true;
                    return new ValueTask<ElicitResult>(new ElicitResult());
                },
            }
        });

        var result = await client.CallToolAsync("TestUrlElicitationMissingId", cancellationToken: TestContext.Current.CancellationToken);

        Assert.True(result.IsError);
        var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
        Assert.Equal("An error occurred invoking 'TestUrlElicitationMissingId': URL mode elicitation requests require an elicitation ID.", textContent.Text);
        Assert.False(elicitationHandlerCalled);
    }

    [Fact]
    public async Task UrlElicitationRequired_Exception_Propagates_To_Client()
    {
        await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
        {
            Capabilities = new ClientCapabilities
            {
                Elicitation = new ElicitationCapability
                {
                    Url = new UrlElicitationCapability(),
                }
            }
        });

        var exception = await Assert.ThrowsAsync<UrlElicitationRequiredException>(
            async () => await client.CallToolAsync("TestUrlElicitationRequired", cancellationToken: TestContext.Current.CancellationToken));

        Assert.Equal(McpErrorCode.UrlElicitationRequired, exception.ErrorCode);

        var elicitation = Assert.Single(exception.Elicitations);
        Assert.Equal("url", elicitation.Mode);
        Assert.Equal("elicitation-required-id", elicitation.ElicitationId);
        Assert.Equal("https://auth.example.com/connect?elicitationId=elicitation-required-id", elicitation.Url);
        Assert.Equal("Authorization is required to access Example Co.", elicitation.Message);
    }

    private ElicitationCapability AssertServerElicitationCapability()
    {
        var capabilities = Server.ClientCapabilities;
        Assert.NotNull(capabilities);
        Assert.NotNull(capabilities.Elicitation);
        return capabilities.Elicitation;
    }

    private sealed class TestForm
    {
        public required string Value { get; set; }
    }
}
